// Image view implmenentation for Gtk+
// Copyright 2006 (C) Ralph Thomas

#include <view/image.h>
#include <view/gtk/common.h>
extern "C" {
#include <ui-image.h> // from libeog
#include <image-view.h>
}
#include <binder.hpp>
#include <gtk/os_utilities.hpp>
#include <util/thread.h>
#include <toolkit/application.h>
#include <boost/shared_ptr.hpp>
#include <boost/thread/xtime.hpp>
#include <iostream>

namespace boost {
	boost::xtime operator-( const boost::xtime& lhs, const boost::xtime& rhs ) {
		boost::xtime result;
		result.nsec = lhs.nsec - rhs.nsec;
		result.sec = lhs.sec - rhs.sec;
		if( result.nsec < 0 ) result.nsec += 1000000000;
		return result;
	}
	int operator>( const boost::xtime& lhs, const boost::xtime& rhs ) {
		if( lhs.sec > rhs.sec ) return true;
		if( (lhs.sec == rhs.sec) && (lhs.nsec > rhs.nsec) ) return true;
		return false;
	}
}

namespace view {
	class gtk_image {
		typedef boost::shared_ptr<std::streambuf> shared_streambuf_t;
		adobe::auto_ptr<GtkWidget>	image_m;
		adobe::binder_helper_t		zoom_m;
		GdkPixbuf*					source_pixbuf_m;
		//
		/// Class to decode an image from a streambuf.
		//
		struct decode_image_streambuf {
			shared_streambuf_t	streambuf_m;
			GdkPixbufLoader*	loader_m;
			boost::xtime		timeout_m;
			unsigned int		idle_id_m;

			decode_image_streambuf() : loader_m(0), idle_id_m(0) {
				timeout_m.sec = 0;
				timeout_m.nsec = 1000000 * 75; // 75 milliseconds.
			}
			~decode_image_streambuf() {
				if( idle_id_m ) g_source_remove( idle_id_m );
				if( loader_m ) {
					gdk_pixbuf_loader_close( loader_m, NULL );
					g_object_unref( loader_m );
				}
			}
			void run(const adobe::any_regular_t& buf, GdkPixbufLoader* loader) {
				streambuf_m = buf.cast<shared_streambuf_t>();
				if( loader_m ) {
					gdk_pixbuf_loader_close( loader_m, NULL );
					g_object_unref( loader_m );
				}
				loader_m = loader;
				idle_id_m = g_idle_add( &decode_image_streambuf::idle_handler, this );
			}
		  private:
			bool do_decode() {
				static char buffer[1024];
				std::streamsize read = 1;
				bool goodread = true;
				boost::xtime start, current;
				boost::xtime_get( &start, boost::TIME_UTC );
				current = start;
				//
				// Read 4k into the loader until we run out of time.
				//
				while( (timeout_m > (current - start)) && goodread && (read > 0) ) {
					read = streambuf_m->sgetn( buffer, 1024 );
					goodread = gdk_pixbuf_loader_write( loader_m, (unsigned char*)buffer, read, 0 );
					boost::xtime_get( &current, boost::TIME_UTC );
				}
				return goodread && (read == 1024);
			}
			static gboolean idle_handler( gpointer data ) {
				decode_image_streambuf* us = reinterpret_cast<decode_image_streambuf*>( data );
				bool more = us->do_decode();
				if( !more )
					us->idle_id_m = 0;
				return more;
			}
		};
		decode_image_streambuf		image_decoder_m;
		void set_zoom( const adobe::any_regular_t& z ) {
			long zoom( 100 );
			if( !z.cast( zoom ) ) return;
			double zfactor = zoom / 100.0;
			image_view_set_zoom( IMAGE_VIEW( ui_image_get_image_view( UI_IMAGE( image_m.get() ) ) ), zfactor, zfactor, false, 0, 0 );
		}
		void fit_zoom() {
			ui_image_zoom_fit( UI_IMAGE( image_m.get() ) );
			double xfactor, yfactor;
			image_view_get_zoom( IMAGE_VIEW( ui_image_get_image_view( UI_IMAGE( image_m.get() ) ) ), &xfactor, &yfactor );
			long zoom( (long)(xfactor * 100.0) );
			zoom_m.set( adobe::any_regular_t( zoom ) );
		}
		//
		// Unfortunately the signal wrapper stuff only works on subclasses of
		// GtkWidget, and GdkPixbufLoader is a subclass of GObject...
		//
		static void area_updated_s( GdkPixbufLoader*, gint x, gint y, gint w, gint h, void* data ) {
			gtk_image* us( reinterpret_cast<gtk_image*>( data ) );
			if( !us ) return;
			//
			// Make the image widget draw this new data
			//
			gtk_widget_queue_draw( us->image_m.get() );
		}
		static void pixbuf_prepared_s( GdkPixbufLoader* loader, void* data ) {
			gtk_image* us( reinterpret_cast<gtk_image*>( data ) );
			//
			// Clear out whatever image is currently visible.
			//
			if( !us ) return;
			if( us->source_pixbuf_m ) g_object_unref( us->source_pixbuf_m );
			//
			// Get the pixbuf that's going to have data read in.
			//
			us->source_pixbuf_m = gdk_pixbuf_loader_get_pixbuf( loader );
			g_object_ref( us->source_pixbuf_m );
			gdk_pixbuf_fill( us->source_pixbuf_m, 0 );
			//
			// Tell the image to use this new pixbuf.
			//
			image_view_set_pixbuf( IMAGE_VIEW( ui_image_get_image_view( UI_IMAGE( us->image_m.get() ) ) ), us->source_pixbuf_m );
			us->fit_zoom();
		}
	  public:
		gtk_image( const adobe::any_regular_t& zoom, adobe::sheet_t& sheet ) : image_m( ui_image_new() ), zoom_m( sheet ), source_pixbuf_m( 0 ) {
			zoom_m.initialize( zoom, boost::bind( &gtk_image::set_zoom, this, _1 ), 0 );
			gtk_widget_set_name( ui_image_get_image_view( UI_IMAGE( image_m.get() ) ), "imageview" );
		}
		~gtk_image() {
			if( source_pixbuf_m ) g_object_unref( source_pixbuf_m );
		}
		void update_image( const adobe::any_regular_t& value ) {
			//
			// Release any image we already have.
			//
			if( source_pixbuf_m ) {
				g_object_unref( source_pixbuf_m );
				source_pixbuf_m = 0;
				image_view_set_pixbuf( IMAGE_VIEW( ui_image_get_image_view( UI_IMAGE( image_m.get() ) ) ), 0 );
			}
			//
			// Try to extract the image -- the data is in a streambuf.
			//
			if( value.type() == typeid(shared_streambuf_t) ) {
				GdkPixbufLoader* pixf( gdk_pixbuf_loader_new() );
				if( !pixf ) return;
				g_signal_connect_after( pixf, "area-prepared", G_CALLBACK( &gtk_image::pixbuf_prepared_s ), this );
				g_signal_connect_after( pixf, "area-updated", G_CALLBACK( &gtk_image::area_updated_s ), this );
				image_decoder_m.run( value, pixf );
				return;
			}
			std::cerr << "imageview: unsupported data type: " << value.type().name() << std::endl;
		}
		void place( const adobe::place_data_t& position ) {
			adobe::implementation::set_bounds_widget( image_m.get(), position );
			toolkit::application::getInstance().executeInMain( boost::bind( &gtk_image::fit_zoom, this ) );
		}
		void measure( const adobe::extents_t& extents ) {}
		GtkWidget* get() const { return image_m.get(); }
	};
	adobe::widget_node_t image_factory(
		const adobe::dictionary_t& 	parameters,
		const adobe::widget_node_t&	parent,
		const adobe::factory_token_t&	token )
	{
		//
		// It's invalid to not have a "bind" parameter.
		//
		adobe::any_regular_t bind = parameters.at( adobe::static_name_t( "bind" ) );
		adobe::any_regular_t zoom;
		if( parameters.count( adobe::static_name_t( "zoom" ) ) )
			zoom = parameters.at( adobe::static_name_t( "zoom" ) );
		//
		// The Gtk icon widget is really simple. It's a GtkImage
		// that is assigned a fixed size in Eve.
		//
		gtk_image* image( new gtk_image( zoom, token.sheet_m ) );
		token.client_holder_m.assemblage_m.delete_on_destruction( image );
		//
		// Create a binder to look after keeping the icon up-to-date.
		//
		adobe::binder_helper_t* binder = new adobe::binder_helper_t( token.sheet_m );
		binder->initialize( bind, boost::bind( &gtk_image::update_image, image, _1 ), 0 );
		token.client_holder_m.assemblage_m.delete_on_destruction( binder );
		//
		// Insert this widget into it's parent.
		//
		GtkWidget* tmp(image->get());
		token.display_m.insert( parent.display_token_m, tmp );
		//
		// Bind the calculate and place signals for Eve.
		//
		adobe::layout_attributes_t attributes;
		attributes.width() = 100;
		attributes.height() = 100;
		//
		// Register the widget with Eve.
		//
		adobe::any_placeable_t* p(new adobe::any_placeable_t(boost::ref(*image)));
		adobe::eve_t::iterator eve_token(
			token.client_holder_m.eve_m.add_view_element( parent.eve_token_m,
					attributes,
					false,
					parameters,
					boost::ref(*p)
				)
			);
		//
		// Return the new icon view.
		//
		return adobe::widget_node_t( parent.size_m, eve_token, tmp, parent.keyboard_token_m );
	}
}
